DirectByteBuffer堆外内存申请、回收 您所在的位置:网站首页 java nio库 DirectByteBuffer堆外内存申请、回收

DirectByteBuffer堆外内存申请、回收

#DirectByteBuffer堆外内存申请、回收| 来源: 网络整理| 查看: 265

JVM中对象在内存中的分布如下:

新生代:一般来说新创建的对象都分配在这里;年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面。年老代中的对象保存的时间更久。永久代:这里面存放的是class相关的信息,一般是不会进行垃圾回收的。

JVM会替我们执行垃圾回收,主要包括young gc和full gc。jvm内存溢出可以通过jmap -heap或者jstat -gcutil工具来诊断。

1、ByteBuffer堆外内存介绍

ByteBuffer堆外内存使用:

从nio时代开始,可以使用ByteBuffer等类来操纵堆外内存了,使用ByteBuffer分配本地内存则非常简单,直接:ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);可以通过指定JVM参数来确定堆外内存大小限制:-XX:MaxDirectMemorySize=512m对于这种direct buffer内存不够的时候会抛出错误: java.lang.OutOfMemoryError: Direct buffer memory

堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。最后,JDK存在一些direct buffer的bug(比如这个和这个),可能引发OOM,所以也不妨升级JDK的版本看能否解决问题。

 

在C语言的内存分配和释放函数malloc/free,必须要一一对应,否则就会出现内存泄露或者是野指针的非法访问。java中ByteBuffer申请的堆外内存需要手动释放吗?ByteBuffer申请的堆外内存也是由GC负责回收的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有,当堆内的引用被gc回收时通过虚拟引用回收其占用的堆外内存!(前提是没有关闭DisableExplicitGC)

我们先简单看一个例子:

/** * @VM args:-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails * -XX:+DisableExplicitGC //增加此参数会内存溢出java.lang.OutOfMemoryError: Direct buffer memory */ public static void TestDirectByteBuffer() { List list = new ArrayList(); while(true) { ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024); //list.add(buffer); } }

1)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails

代码会一直运行下午,同时会看到系统频繁的进行垃圾回收;

2)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails -XX:+DisableExplicitGC

增加了-XX:+DisableExplicitGC,这个参数作用是禁止显示调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果,相当于是没有这行代码一样。

显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说使用了java nio中的direct memory,那么-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险。(后面会讲到Direct Memory的申请时会使用System.gc())

3)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails

JVM参数把-XX:+DisableExplicitGC去掉,代码将list.add(buffer); 注释放开再次运行,仍然会报java.lang.OutOfMemoryError: Direct buffer memory

原因是堆内list把堆外内存对象的引用一直持有,导致堆内的引用无法被gc,从而堆外内存也无法回收。

2、ByteBuffer堆外内存回收的矛盾点:

我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),或是内存不足,才进行垃圾回收。使用ByteBuffer堆外内存回收的矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象(引用),才能释放堆外内存,但是我们又不能强制JVM释放堆内存。例如:ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

再者,假设堆内 ByteBuffer对象的引用升级到了老年代,导致这个引用会长期存在无法回收,这时堆外的内存将长期无法得到回收。

 

3、ByteBuffer源码分析,堆外内存申请:

ByteBuffer.allocateDirect(cap);

进行内存申请的时候,会调用:DirectByteBuffer(int cap)构造函数,如下:

DirectByteBuffer(int cap) { // package-private // 初始化Buffer的四个核心属性 super(-1, 0, cap, cap); // 判断是否需要页面对齐,通过参数-XX:+PageAlignDirectMemory控制,默认为false boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); // 确保有足够内存 long size = Math.max(1L, (long)cap + (pa ? ps : 0)); Bits.reserveMemory(size, cap); long base = 0; try { // 调用unsafe方法分配内存 base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { // 分配失败,释放内存 Bits.unreserveMemory(size, cap); throw x; } // 初始化内存空间为0 unsafe.setMemory(base, size, (byte) 0); // 设置内存起始地址 if (pa && (base % ps != 0)) { address = base + ps - (base & (ps - 1)); } else { address = base; } // 使用Cleaner机制注册内存回收处理函数 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }

 

 

首先,这个构造函数里的Bits.reserveMemory(size, cap)方法判断是否有足够的空间可供申请:

// 该方法主要用于判断申请的堆外内存是否超过了用例指定的最大值 // 如果还有足够空间可以申请,则更新对应的变量 // 如果已经没有空间可以申请,则抛出OOME // 参数解释: // size:根据是否按页对齐,得到的真实需要申请的内存大小 // cap:用户指定需要的内存大小( -1) directMemory = l; } } //... }

所以默认情况下,可以申请的DirectByteBuffer大小为Runtime.getRuntime().maxMemory(),而这个值等于可用的最大Java堆大小,也就是我们-Xmx参数指定的值。

参考:http://www.importnew.com/29817.html

 

 

 

 



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有